
/* GCSx
** UNDO.CPP
**
** Editor undo and redo functionality
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

int UndoBuffer::undoHeaderSize = (sizeof(UndoBuffer::UndoHeader) + sizeof(Uint32) - 1) / sizeof(Uint32); // Size in Uint32s
#ifndef NDEBUG
int UndoBuffer::undoThresh = -1;

void UndoBuffer::setUndoThreshold(int level) { start_func
    if (level) undoThresh = level - 1;
    else undoThresh = -1;
}
#endif

UndoBuffer::UndoBuffer(WorldEdit* myWorld) { start_func
    world = myWorld;
    requestedSize = UNDO_DEFAULT_BUFFER;
    undoSizeAlloc = 0;
    undoBuffer = NULL;
    undoPos = 0;
    inUndo = 0;
    suspendUndo = 0;
    undoBlock = 0;
    undoBlockDidFirst = 0;
    undoBlockFirstPos = 0;
    undoNonUndoable = 0;
}

UndoBuffer::~UndoBuffer() { start_func
    // Delete allocated objects etc.
    clearUndo();
}

void UndoBuffer::disableUndo() { start_func
    suspendUndo = 1;
}

void UndoBuffer::enableUndo() { start_func
    suspendUndo = 0;
}

int UndoBuffer::bufferSize(int requested) { start_func
    // Check range
    if (requested < 0) requested = 0;
    if (requested > UNDO_MAX_BUFFER) requested = UNDO_MAX_BUFFER;

    if (requested != requestedSize) {
        clearUndo();
        requestedSize = requested;
    }
    
    return requestedSize;
}

Uint32* UndoBuffer::createUndoSpace(UndoType chunkType, int id, int subid, int xPos, int yPos, int xSize, int ySize, Window* srcWin, Uint32 data1, Uint32 data2, Uint32 data3, void* item1, void* item2, void* item3) throw_Undo { start_func
    Uint32* result;
    UndoHeader* header;
    
    // If in an undo, "cancel" any further undos by returning NULL
    if (inUndo) return NULL;
    
    // If in a block, cancel further undos if user was already warned
    if ((undoBlock) && (undoNonUndoable)) return NULL;

    // Cancel if undo has been explicitly disabled
    if (!requestedSize) return NULL;

    if (suspendUndo) {
        clearUndo();
        return NULL;
    }
    
#ifndef NDEBUG
    // Threshold enabled?
    if (undoThresh == 0) {
        warnUser();
        return NULL;
    }
    if (undoThresh > 0) --undoThresh;
#endif

    if (undoBuffer == NULL) {
        // 256 = number of Uint32s in 1k
        undoBuffer = new(nothrow) Uint32[requestedSize * 256];
            
        if (undoBuffer == NULL) {
            warnUser();
            return NULL;
        }
        
        undoSizeAlloc = requestedSize * 256;
        undoPos = 0;
        header = (UndoHeader*)undoBuffer;
        header->sourceWindow = 0;
        header->prevChunkSize = 0;
        header->type = UNDO_EMPTY;
        header->id = 0;
        header->xSize = 0;
        header->ySize = 0;
    }
    
    // Too big?
    if (xSize * ySize + undoHeaderSize * 2 >= undoSizeAlloc) {
        warnUser();
        return NULL;
    }
    
    // Too big to fit with the rest of our merged data?
    if ((undoBlock) && (undoBlockDidFirst) && (xSize * ySize + undoHeaderSize * 2 + undoPos - undoBlockFirstPos >= undoSizeAlloc)) {
        warnUser();
        return NULL;
    }
    
    // If we add at undoPos, we need to fit there the entire new chunk,
    // plus header, plus header of empty chunk; find out how much needs to be deleted
    int deletingAt = 0;
    while (xSize * ySize + undoHeaderSize * 2 + undoPos - deletingAt > undoSizeAlloc) {
        int isMerged = 1;

        // Delete any merged chunks as well
        while (isMerged) {
            // Delete first item in list, retry
            header = (UndoHeader*)(undoBuffer + deletingAt);
            deleteUndoBlock(deletingAt);
            // Move down to delete initial chunk
            deletingAt += header->xSize * header->ySize + undoHeaderSize;
            // New initial chunk's "previous size" is now 0
            header = (UndoHeader*)(undoBuffer + deletingAt);
            header->prevChunkSize = 0;
            // Is next block merged with the one we just did?
            isMerged = header->type & UNDO_MERGE;
        }
    }
    
    // Move data to finalize deletion
    if (deletingAt) {
        memmove(undoBuffer, undoBuffer + deletingAt, (undoPos + undoHeaderSize - deletingAt) * 4);
        undoPos -= deletingAt;
    }
    
    // Delete any redo chunks here or later
    clearRedo();
    
    // There's now room for the new chunk- create it.
    // Current 'blank' or redo chunk at undoPos already has proper "previous size" value
    // This also overwrites any existing redo- no redo allowed after an action
    result = undoBuffer + undoPos + undoHeaderSize;
    header = (UndoHeader*)(undoBuffer + undoPos);
    header->sourceWindow = srcWin ? srcWin->getId() : 0;
    header->type = chunkType;
    // (part of a block?)
    if (undoBlock) {
        if (undoBlockDidFirst) header->type = (UndoType)(header->type | UNDO_MERGE);
        else {
            undoBlockFirstPos = undoPos;
            undoBlockDidFirst = 1;
        }
    }
    header->id = id;
    header->subid = subid;
    header->xSize = xSize;
    header->ySize = ySize;
    header->xPos = xPos;
    header->yPos = yPos;
    header->data1 = data1;
    header->data2 = data2;
    header->data3 = data3;
    header->item1 = item1;
    header->item2 = item2;
    header->item3 = item3;
    
    // Create new 'blank' chunk
    undoPos += xSize * ySize + undoHeaderSize;
    header = (UndoHeader*)(undoBuffer + undoPos);
    header->sourceWindow = 0;
    header->prevChunkSize = xSize * ySize + undoHeaderSize;
    header->type = UNDO_EMPTY;
    header->id = 0;
    header->xSize = 0;
    header->ySize = 0;

    // Done
    return result;
}

void UndoBuffer::preUndoBlock() { start_func
    if ((inUndo) || (suspendUndo)) return;
    if (!undoBlock) {
        undoBlockDidFirst = 0;
        undoBlockFirstPos = 0;
        undoNonUndoable = 0;
    }
    ++undoBlock;
    debugWrite(DEBUG_UNDO, "undo: pre-block (%d)", undoBlock);
}

void UndoBuffer::postUndoBlock() { start_func
    if ((inUndo) || (suspendUndo)) return;
    // Could already be at zero if blocks were nested and the inner one
    // threw an exception
    if (undoBlock) {
        debugWrite(DEBUG_UNDO, "undo: post-block (%d)", undoBlock);
        --undoBlock;
    }
}

void UndoBuffer::cancelLastUndo() { start_func
    UndoHeader* header = (UndoHeader*)(undoBuffer + undoPos);
    header->type = (UndoType)(header->type & ~UNDO_MERGE);
    undoPos -= header->prevChunkSize;
    clearRedo();
}

void UndoBuffer::cancelUndoBlock() { start_func
    // Must be in a block
    if (undoBlock) {
        if (!inUndo) debugWrite(DEBUG_UNDO, "undo: cancel block (%d)", undoBlock);

        // (no longer in a block)
        undoBlock = 0;

        if (undoBlockDidFirst) {
            undoPerform();
            clearRedo();
        }
    }
}

void UndoBuffer::storeUndoLayerTile(int layerId, int x, int y, int w, int h, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer tiles");

    // Find layer
    LayerEdit* layer = world->findLayer(layerId);
    assert(layer);
    assert(layer->getType() == Layer::LAYER_TILE);
    
    int layerWidth = layer->getXSize();
    int layerHeight = layer->getYSize();

    // Bound
    if ((x >= layerWidth) || (y >= layerHeight) || (x + w <= 0) || (y + h <= 0)) return;
    if (x < 0) {
        w += x;
        x = 0;
    }
    if (y < 0) {
        h += y;
        y = 0;
    }
    if (x + w > layerWidth) w = layerWidth - x;
    if (y + h > layerHeight) h = layerHeight - y;
    
    // Fx/Coll present?
    int ext = layer->getUsesExt();
    int fx = layer->getUsesFx();
    if ((fx) || (ext)) preUndoBlock();

    Uint32* bufferExt = NULL;
    Uint32* bufferFx = NULL;
    Uint32* buffer = createUndoSpace(UNDO_LAYERTILE, layerId, 0, x, y, w, h, srcWin);
    if (!buffer) {
        if ((fx) || (ext)) postUndoBlock();
        return;
    }
    if (ext) {
        if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer extended data");
        bufferExt = createUndoSpace(UNDO_LAYERTILEEXT, layerId, 0, x, y, w, h, srcWin);
        if (!bufferExt) {
            postUndoBlock();
            return;
        }
    }
    if (fx) {
        if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer effects");
        bufferFx = createUndoSpace(UNDO_LAYERTILEFX, layerId, 0, x, y, w, h, srcWin);
        if (!bufferFx) {
            postUndoBlock();
            return;
        }
    }
    
    Rect rect = { x, y, w, h };
    layer->loadLayer(buffer, bufferExt, bufferFx, w, rect);

    if ((fx) || (ext)) postUndoBlock();
}

void UndoBuffer::storeUndoTile(int tileSetId, int tileNum, int x, int y, int w, int h, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tile");

    // Find tileset
    TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(tileSetId));
    assert(tiles);

    int tileWidth = tiles->getWidth();
    int tileHeight = tiles->getHeight();

    // Bound
    if ((x >= tileWidth) || (y >= tileHeight) || (x + w <= 0) || (y + h <= 0)) return;
    if (x < 0) {
        w += x;
        x = 0;
    }
    if (y < 0) {
        h += y;
        y = 0;
    }
    if (x + w > tileWidth) w = tileWidth - x;
    if (y + h > tileHeight) h = tileHeight - y;

    Uint32* buffer;
    buffer = createUndoSpace(UNDO_TILE, tileSetId, tileNum, x, y, w, h, srcWin);
    if (!buffer) return;

    int pitch;
    const Uint8* source;
    int lineSize = w * 4;

    pitch = tiles->viewTilePitch();
    source = tiles->viewTileData(tileNum) + y * pitch + x * 4;

    // @TODO: matrixCopy()
    for (; h > 0; --h) {
        memcpy(buffer, source, lineSize);
        buffer += w;
        source += pitch;
    }
}

void UndoBuffer::storeUndoColl(int tileSetId, int collNum, int x, int y, int w, int h, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store collision map");

    // Find tileset
    TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(tileSetId));
    assert(tiles);

    int tileWidth = tiles->getWidth();
    int tileHeight = tiles->getHeight();

    // Bound
    if ((x >= tileWidth) || (y >= tileHeight) || (x + w <= 0) || (y + h <= 0)) return;
    if (x < 0) {
        w += x;
        x = 0;
    }
    if (y < 0) {
        h += y;
        y = 0;
    }
    if (x + w > tileWidth) w = tileWidth - x;
    if (y + h > tileHeight) h = tileHeight - y;
    
    // Everything is in chunks 32 wide
    if (x & 31) {
        w += x & 31;
        x -= x & 31;
    }
    x /= 32;
    w = (w + 31) / 32;

    Uint32* buffer;
    buffer = createUndoSpace(UNDO_TILECOLL, tileSetId, collNum, x, y, w, h, srcWin);
    if (!buffer) return;

    const Uint32* source;
    source = tiles->viewCollData(collNum) + w * tileHeight + y;

    // @TODO: matrixCopy()
    for (; w > 0; --w) {
        memcpy(buffer, source, h);
        buffer += h;
        source += tileHeight;
    }
}

void UndoBuffer::storeUndoLayerSwap(int sceneId, int layer1, int layer2, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer swap");
    createUndoSpace(UNDO_LAYERMOVE, sceneId, 0, layer1, layer2, 0, 0, srcWin);
}

void UndoBuffer::storeUndoTileGlyph(int tileSetId, int tileNum, int oldWidth, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tile glyph");
    createUndoSpace(UNDO_TILEGLYPH, tileSetId, tileNum, oldWidth, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoLayerType(int layerId, int oldType, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer type");
    createUndoSpace(UNDO_LAYERTYPE, layerId, 0, oldType, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoLayerSize(int layerId, int oldWidth, int oldHeight, Uint32* data, Uint32* dataExt, Uint32* dataFx, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer size");

    // If undo space not created, we delete saved buffers now, unless we're "in undo"
    if ((!createUndoSpace(UNDO_LAYERSIZE, layerId, 0, oldWidth, oldHeight, 0, 0, srcWin, 0, 0, 0, data, dataExt, dataFx)) && (!inUndo)) {
        delete[] data;
        delete[] dataExt;
        delete[] dataFx;
    }
}

void UndoBuffer::storeUndoLayerFx(int layerId, int oldUsesFx, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer effects flag");
    createUndoSpace(UNDO_LAYERFX, layerId, 0, oldUsesFx, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoLayerExt(int layerId, int oldUsesExt, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer extended data flag");
    createUndoSpace(UNDO_LAYEREXT, layerId, 0, oldUsesExt, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoLayerTileSet(int layerId, int oldTileSet, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer tileset");
    createUndoSpace(UNDO_LAYERTILESET, layerId, 0, oldTileSet, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoLayerSpawnSelection(set<int>* selectionData, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn selection");
    assert(selectionData);
    
    set<int>* newData = new set<int>(*selectionData);
    try {
        Uint32* buffer = createUndoSpace(UNDO_LAYERSPAWNSEL, 0, 0, 0, 0, 0, 0, srcWin, 0, 0, 0, selectionData, newData);
        if (!buffer) {
            delete newData;
            return;
        }
    }
    catch (...) {
        delete newData;
        throw;
    }
}

void UndoBuffer::storeUndoLayerTileSelection(Uint8** selectionData, int x, int y, int w, int h, int pitch, int selectXOffs, int selectYOffs, int selectType, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer selection");
    assert(selectionData);
    assert((x + w) / 8 <= pitch);

    // place x in byte terms, round down, alter width to cover
    w += x % 8;
    x = x / 8;
    
    // break width into byte width and 32bit width
    int bytewidth = w / 8;
    if (w % 8) ++bytewidth;
    
    int width32 = w / 32;
    if (w % 32) ++width32;

    Uint32* buffer = createUndoSpace(UNDO_LAYERTILESEL, bytewidth, pitch, x, y, width32, h, srcWin, selectXOffs, selectYOffs, selectType, selectionData);
    if (!buffer) return;

    matrixCopy(*selectionData + x + y * pitch, buffer, bytewidth, h, pitch, width32 * 4);
}

void UndoBuffer::storeUndoLayerTileCur(Uint32 layerAffect, Uint32 layerView, Uint32 layerDim, Window* srcWin) throw_Undo { start_func
    createUndoSpace(UNDO_LAYERTILECUR, 0, 0, 0, 0, 0, 0, srcWin, layerAffect, layerView, layerDim);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer affected/view");
}

void UndoBuffer::storeUndoLayerTileTemp(Uint32** data, Uint32** dataExt, Uint32** dataFx, int x, int y, int w, int h, int pitch, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer floating selection");
    assert(data);
    assert(x + w <= pitch);
    
    // NULL ptr at *dataFx also means no Fx data; same for ext
    if (dataExt) {
        if (!*dataExt) dataExt = NULL;
    }
    if (dataFx) {
        if (!*dataFx) dataFx = NULL;
    }

    Uint32* buffer = createUndoSpace(UNDO_LAYERTILETMP, 0, pitch, x, y, w, h * (1 + (dataFx ? 1 : 0) + (dataExt ? 1 : 0)), srcWin, 0, 0, 0, data, dataExt, dataFx);
    if (!buffer) return;

    matrixCopy(*data + x + y * pitch, buffer, w * 4, h, pitch * 4, w * 4);
    if (dataExt) matrixCopy(*dataExt + x + y * pitch, buffer + w * h, w * 4, h, pitch * 4, w * 4);
    if (dataFx) matrixCopy(*dataFx + x + y * pitch, buffer + (w * h) * (dataExt ? 2 : 1), w * 4, h, pitch * 4, w * 4);
}

void UndoBuffer::storeUndoPaintSelection(SDL_Surface** selectionData, int x, int y, int w, int h, int selectXOffs, int selectYOffs, int selectType, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tile selection");
    assert(selectionData);

    int tileWidth = (*selectionData)->w;
    int tileHeight = (*selectionData)->h;

    // Bound
    if ((x >= tileWidth) || (y >= tileHeight) || (x + w <= 0) || (y + h <= 0)) return;
    if (x < 0) {
        w += x;
        x = 0;
    }
    if (y < 0) {
        h += y;
        y = 0;
    }
    if (x + w > tileWidth) w = tileWidth - x;
    if (y + h > tileHeight) h = tileHeight - y;

    Uint32* buffer = createUndoSpace(UNDO_PAINTSEL, 0, 0, x, y, w, h, srcWin, selectXOffs, selectYOffs, selectType, selectionData);
    if (!buffer) return;
    
    int pitch = (*selectionData)->pitch;
    const Uint8* source = (Uint8*)((*selectionData)->pixels) + y * pitch + x * 4;
    int lineSize = w * 4;
    
    // @TODO: matrixCopy()    
    for (; h > 0; --h) {
        memcpy(buffer, source, lineSize);
        buffer += w;
        source += pitch;
    }
}

void UndoBuffer::storeUndoName(UndoType type, int id, const string& oldName, const string& newName, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store name");
    assert((type == UNDO_FOLDERNAME) || (type == UNDO_SPAWNNAME) || (type == UNDO_WORLDNAME) || (type == UNDO_SCENENAME) || (type == UNDO_TILENAME) || (type == UNDO_LAYERNAME) || (type == UNDO_ANIMGROUPNAME) || (type == UNDO_SCRIPTNAME));

    Uint32* buffer;
    int size = (max(oldName.size(), newName.size()) + 3) / 4;
    buffer = createUndoSpace(type, id, 0, oldName.size(), 0, size, 1, srcWin);
    if (!buffer) return;
    
    memcpy(buffer, oldName.c_str(), oldName.size());
}

int UndoBuffer::storeUndoLayerDelete(int id, int sceneId, int pos, LayerEdit* object, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer deletion");

    // If undo space not created, we delete object now, unless we're "in undo"
    if ((!createUndoSpace(UNDO_LAYERDELETE, id, sceneId, pos, 0, 0, 0, srcWin, 0, 0, 0, object)) && (!inUndo)) {
        return 0;
    }
    return 1;
}

int UndoBuffer::storeUndoLayerCreate(int id, int sceneId, int pos, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store layer creation");
    if (createUndoSpace(UNDO_LAYERCREATE, id, sceneId, pos, 0, 0, 0, srcWin)) return 1;
    return 0;
}

int UndoBuffer::storeUndoSpawnDelete(int id, int layerId, SpawnEdit* object, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn deletion");

    // If undo space not created, we delete object now, unless we're "in undo"
    if ((!createUndoSpace(UNDO_SPAWNDELETE, id, layerId, 0, 0, 0, 0, srcWin, 0, 0, 0, object)) && (!inUndo)) {
        return 0;
    }
    return 1;
}

int UndoBuffer::storeUndoSpawnCreate(int id, int layerId, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn creation");
    if (createUndoSpace(UNDO_SPAWNCREATE, id, layerId, 0, 0, 0, 0, srcWin)) return 1;
    return 0;
}

int UndoBuffer::storeUndoCreate(UndoType type, int id, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store creation");
    assert((type == UNDO_FOLDERCREATE) || (type == UNDO_SCENECREATE) || (type == UNDO_TILECREATE) || (type == UNDO_SCENECREATE) || (type == UNDO_ANIMGROUPCREATE) || (type == UNDO_SCRIPTCREATE));
    if (createUndoSpace(type, id, 0, 0, 0, 0, 0, srcWin)) return 1;
    return 0;
}

int UndoBuffer::storeUndoDelete(UndoType type, int id, void* object, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store deletion");
    assert((type == UNDO_FOLDERDELETE) || (type == UNDO_TILEDELETE) || (type == UNDO_SCENEDELETE) || (type == UNDO_ANIMGROUPDELETE) || (type == UNDO_SCRIPTDELETE));

    // If undo space not created, we delete object now, unless we're "in undo"
    if ((!createUndoSpace(type, id, 0, 0, 0, 0, 0, srcWin, 0, 0, 0, object)) && (!inUndo)) {
        return 0;
    }
    return 1;
}

void UndoBuffer::storeUndoSpawnMove(int id, int layerId, int oldX, int oldY, Window* srcWin) throw_Undo { start_func
    createUndoSpace(UNDO_SPAWNMOVE, id, layerId, oldX, oldY, 0, 0, srcWin);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn move");
}

void UndoBuffer::storeUndoSpawnSprite(int id, int layerId, int animId, int tileId, int subId, Window* srcWin) throw_Undo { start_func
    createUndoSpace(UNDO_SPAWNSPRITE, id, layerId, 0, 0, 0, 0, srcWin, animId, tileId, subId);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn image");
}

void UndoBuffer::storeUndoSpawnScript(int id, int layerId, int scriptId, Window* srcWin) throw_Undo { start_func
    createUndoSpace(UNDO_SPAWNSCRIPT, id, layerId, 0, 0, 0, 0, srcWin, scriptId);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store spawn script");
}

void UndoBuffer::storeUndoFolderAdd(int id, SaveLoad* item, int pos, Window* srcWin) throw_Undo { start_func
    assert(item);
    assert(item->getId());

    createUndoSpace(UNDO_FOLDERADD, id, item->getBlockType(), 0, 0, 0, 0, srcWin, item->getId(), pos);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store folder add");
}

void UndoBuffer::storeUndoFolderRemove(int id, SaveLoad* item, int pos, Window* srcWin) throw_Undo { start_func
    assert(item);
    assert(item->getId());

    createUndoSpace(UNDO_FOLDERREMOVE, id, item->getBlockType(), 0, 0, 0, 0, srcWin, item->getId(), pos);
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store folder add");
}

void UndoBuffer::storeUndoTileSize(int tileSetId, int oldWidth, int oldHeight, int oldCount, int oldMaps, SDL_Surface* surface, Uint32* fontWidths, Uint32* collisionMaps, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tileset size");
    // If undo space not created, we delete saved buffers now, unless we're "in undo"
    if ((!createUndoSpace(UNDO_TILESIZE, tileSetId, 0, oldWidth, oldHeight, 0, 0, srcWin, oldCount, oldMaps, 0, surface, fontWidths, collisionMaps)) && (!inUndo)) {
        if (surface) SDL_FreeSurface(surface);
        delete[] fontWidths;
        delete[] collisionMaps;
    }
}

void UndoBuffer::storeUndoTilePerLine(int tileSetId, int oldPerLine, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tileset per line");
    createUndoSpace(UNDO_TILEPERLINE, tileSetId, 0, oldPerLine, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoTileTrans(int tileSetId, int oldDefaultTransparent, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store tileset transparency flag");
    createUndoSpace(UNDO_TILETRANS, tileSetId, 0, oldDefaultTransparent, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoWorldStart(int oldStart, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store world starting scene");
    createUndoSpace(UNDO_WORLDSTART, 0, 0, oldStart, 0, 0, 0, srcWin);
}

void UndoBuffer::storeUndoScriptDefault(int id, int animId, int tileId, int subId, Window* srcWin) throw_Undo { start_func
    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store script default image");
    createUndoSpace(UNDO_SCRIPTDEFAULT, id, 0, 0, 0, 0, 0, srcWin, animId, tileId, subId);
}

void UndoBuffer::storeUndoScriptModify(int id, const list<string>& source, int lineNum, int count, EditBox* editbox, Window* srcWin) throw_Undo { start_func
    storeUndoScript(UNDO_SCRIPTMODIFY, id, source, lineNum, count, editbox, srcWin);
}

void UndoBuffer::storeUndoScript(UndoType type, int id, const list<string>& source, int lineNum, int count, EditBox* editbox, Window* srcWin) throw_Undo { start_func
    assert(lineNum >= 0);
    assert(count > 0);
    assert((type == UNDO_SCRIPTMODIFY) || (type == UNDO_SCRIPTREMOVE));
    
    // Shortcut to prevent extra work and iterating to already-deleted data
    if (inUndo) return;
    
    if (type == UNDO_SCRIPTMODIFY) debugWrite(DEBUG_UNDO, "undo: store script modify");
    else debugWrite(DEBUG_UNDO, "undo: store script remove");

    list<string>::const_iterator begin = source.begin();
    for (int pos = 0; pos < lineNum; ++pos) {
        ++begin;
    }
    
    list<string>::const_iterator end = begin;
    for (int pos = 0; pos < count; ++pos) {
        ++end;
    }
    
    list<string>* copyScript = new list<string>(begin, end);

    // Is most recent undo block also a MODIFY of the same settings exactly? (and not merged)
    //@TODO: only combine same type of event (type, delete, etc.) this can be done once we
    // add undo descriptions
    if ((type == UNDO_SCRIPTMODIFY) && (undoAvailable())) {
        UndoHeader* header = (UndoHeader*)(undoBuffer + undoPos);
        int prevPos = undoPos - header->prevChunkSize;
        header = (UndoHeader*)(undoBuffer + prevPos);
        if ((header->type == UNDO_SCRIPTMODIFY) && (header->id == id) &&
            (header->xPos == lineNum) && (header->yPos == count) &&
            (header->sourceWindow == (srcWin ? srcWin->getId() : 0)) && (header->item2 == editbox)) {
                // If so, store nothing new; all done!
                delete copyScript;
                return;
        }
    }

    Uint32* buffer = NULL;
    
    try {
        buffer = createUndoSpace(type, id, 0, lineNum, count, sizeof(EditBox::TextPoint[3]) / sizeof(Uint32) + 1, 1, srcWin, 0, 0, 0, copyScript, editbox);
        if (!buffer) {
            delete copyScript;
            return;
        }
    }
    catch (...) {
        delete copyScript;
        throw;
    }
    
    if (editbox) {
        EditBox::TextPoint* tp = (EditBox::TextPoint*)buffer;
        editbox->getCursorData(tp[0], tp[1], tp[2]);
    }
}

void UndoBuffer::storeUndoScriptRemove(int id, const list<string>& source, int lineNum, int count, EditBox* editbox, Window* srcWin) throw_Undo { start_func
    storeUndoScript(UNDO_SCRIPTREMOVE, id, source, lineNum, count, editbox, srcWin);
}

void UndoBuffer::storeUndoScriptInsert(int id, int lineNum, int count, EditBox* editbox, Window* srcWin) throw_Undo { start_func
    assert(lineNum >= 0);
    assert(count > 0);

    if (!inUndo) debugWrite(DEBUG_UNDO, "undo: store script insert");
    Uint32* buffer = createUndoSpace(UNDO_SCRIPTINSERT, id, 0, lineNum, count, sizeof(EditBox::TextPoint[3]) / sizeof(Uint32) + 1, 1, srcWin, 0, 0, 0, NULL, editbox);
    
    if ((buffer) && (editbox)) {
        EditBox::TextPoint* tp = (EditBox::TextPoint*)buffer;
        editbox->getCursorData(tp[0], tp[1], tp[2]);
    }
}

void UndoBuffer::clearUndo() { start_func
    debugWrite(DEBUG_UNDO, "undo: clear all");
    if (undoBuffer) {
        // Scan up to end of any redos
        UndoHeader* header = (UndoHeader*)(undoBuffer + undoPos);
        while (header->type != UNDO_EMPTY) {
            undoPos += header->xSize * header->ySize + undoHeaderSize;
            header = (UndoHeader*)(undoBuffer + undoPos);
        }
    
        // Must clear any allocated objects
        while (undoPos > 0) {
            header = (UndoHeader*)(undoBuffer + undoPos);
            undoPos -= header->prevChunkSize;
            deleteUndoBlock(undoPos);
        }

        delete[] undoBuffer;
        undoBuffer = NULL;
        undoSizeAlloc = 0;
        undoPos = 0;
    }
}

void UndoBuffer::deleteUndoBlock(int offset) { start_func
    UndoHeader* header = (UndoHeader*)(undoBuffer + offset);
    int type = header->type & ~UNDO_MERGE;
    
    switch (type) {
        case UNDO_SCRIPTREMOVE:
        case UNDO_SCRIPTMODIFY:
            delete (list<string>*)header->item1;
            break;

        case UNDO_TILEDELETE:
            delete (TileSetEdit*)header->item1;
            break;

        case UNDO_SCENEDELETE:
            delete (SceneEdit*)header->item1;
            break;

        case UNDO_ANIMGROUPDELETE:
            delete (AnimGroupEdit*)header->item1;
            break;

        case UNDO_FOLDERDELETE:
            delete (FolderEdit*)header->item1;
            break;

        case UNDO_SCRIPTDELETE:
            delete (ScriptEdit*)header->item1;
            break;

        case UNDO_LAYERDELETE:
            delete (LayerEdit*)header->item1;
            break;
            
        case UNDO_SPAWNDELETE:
            delete (SpawnEdit*)header->item1;
            break;
            
        case UNDO_TILESIZE:
            if (header->item1) SDL_FreeSurface((SDL_Surface*)header->item1);
            delete[] (Uint32*)header->item2;
            delete[] (Uint32*)header->item3;
            break;
            
        case UNDO_LAYERSIZE:
            delete[] (Uint32*)header->item1;
            delete[] (Uint32*)header->item2;
            delete[] (Uint32*)header->item3;
            break;
            
        case UNDO_LAYERSPAWNSEL:
            delete (set<int>*)header->item2;
            break;
    }
    
    header->item1 = NULL;
    header->item2 = NULL;
    header->item3 = NULL;
}

void UndoBuffer::warnUser() throw_Undo { start_func
    if (guiConfirmBox("The undo buffer cannot store this action and will be cleared- continue without ability to undo?", "Confirm Permanent Action")) {
        clearUndo();
        // (cancel any further pieces of a current block)
        if (undoBlock) undoNonUndoable = 1;
        return;
    }

    debugWrite(DEBUG_UNDO, "undo: cancel undo");
    
    // Undo anything done as part of a block
    if (undoBlock) {
        if (undoBlockDidFirst) {
            undoPerform();
        }
        undoBlock = 0;
        // Delete any redo chunks here or later
        clearRedo();
    }
    throw UndoException();
}

void UndoBuffer::clearRedo() { start_func
    if (undoBuffer) {
        int redoPos = undoPos;
        UndoHeader* header = (UndoHeader*)(undoBuffer + redoPos);
        while (header->type != UNDO_EMPTY) {
            deleteUndoBlock(redoPos);
            redoPos += header->xSize * header->ySize + undoHeaderSize;
    
            // Actually delete each block from buffer
            header->sourceWindow = 0;
            header->type = UNDO_EMPTY;
            header->id = 0;
            header->xSize = 0;
            header->ySize = 0;
            
            header = (UndoHeader*)(undoBuffer + redoPos);
        }
    }
}

int UndoBuffer::undoAvailable() const { start_func
    // If undo pos isn't pointing at first item, there's undo available
    if ((undoBuffer) && (undoPos > 0)) return 1;
    return 0;
}

int UndoBuffer::redoAvailable() const { start_func
    // If undo pos isn't pointing at an empty item, there's redo available
    if (undoBuffer) {
        UndoHeader* header = (UndoHeader*)(undoBuffer + undoPos);
        if (header->type != UNDO_EMPTY) return 1;
    }
    return 0;
}

void UndoBuffer::redoPerform() { start_func
    undoPerform(1);
}

void UndoBuffer::undoPerform(int doRedo) { start_func
    UndoHeader* header = (UndoHeader*)(undoBuffer + undoPos);
    int didWindowFocus = 0;
    int didWindowQuery = 0;
    int windowDidntExist = 0;
    // Repeat undo/redo?
    int isMerged = 1;
    int cancelUndo = 0;
    inUndo = 1;

    while (isMerged) {
        if (doRedo) {
            debugWrite(DEBUG_UNDO, "undo: perform redo");
            if (!redoAvailable()) {
                inUndo = 0;
                return;
            }
        }
        else {
            debugWrite(DEBUG_UNDO, "undo: perform undo");
            if (!undoAvailable()) {
                inUndo = 0;
                return;
            }
    
            // Move undo pos backward an item
            undoPos -= header->prevChunkSize;
            header = (UndoHeader*)(undoBuffer + undoPos);
            isMerged = header->type & UNDO_MERGE;
        }
        
        const int type = header->type & ~UNDO_MERGE;

        // Verify target window exists, but don't query user if a selection action
        if (!header->sourceWindow) windowDidntExist = 1;
        else if (!didWindowQuery) {
            // Verify existence
            if (!desktop->verifyWindow(header->sourceWindow)) {
                windowDidntExist = 1;
                if ((type != UNDO_LAYERTILETMP) && (type != UNDO_LAYERTILECUR) &&
                    (type != UNDO_LAYERTILESEL) && (type != UNDO_PAINTSEL) &&
                    (type != UNDO_LAYERSPAWNSEL)) {
                    if (!guiConfirmBox("You have already closed the window the original action occurred in- still undo?", "Confirm Undo")) cancelUndo = 1;
                }
            }
        }
        didWindowQuery = 1;

        // Used later in notify
        const int id = header->id;
        const int subid = header->subid;
        const int w = header->xSize;
        const int h = header->ySize;
        const int x = header->xPos;
        const int y = header->yPos;
        const int d1 = header->data1;
        const int d2 = header->data2;
        const int d3 = header->data3;
        Uint32* buffer = undoBuffer + undoPos + undoHeaderSize;
        Uint32* temp = NULL;
        
        if (!cancelUndo) {
            try {
                // Type of item?
                switch (type) {
                    case UNDO_SCRIPTMODIFY:
                    case UNDO_SCRIPTREMOVE:
                    case UNDO_SCRIPTINSERT: {
                        ScriptEdit* script = dynamic_cast<ScriptEdit*>(world->findScript(id));
                        assert(script);
                        script->markLock(); // Exception point
                        list<string>::iterator cPos = script->getSource().begin();
                        for (int pos = 0; pos < x; ++pos) {
                            ++cPos;
                        }

                        EditBox* edit = (EditBox*)header->item2;
                        EditBox::TextPoint ins, selBegin, selEnd;
                        if ((edit) && (!windowDidntExist)) {
                            EditBox::TextPoint* tp = (EditBox::TextPoint*)buffer;
                            edit->getCursorData(ins, selBegin, selEnd);
                            swap(ins, tp[0]);
                            swap(selBegin, tp[1]);
                            swap(selEnd, tp[2]);
                        }

                        if (type == UNDO_SCRIPTMODIFY) {
                            debugWrite(DEBUG_UNDO, "undo: modify script modify");
                            script->codeModifiedPre(ScriptEdit::SCRIPT_MODIFY_LINE, x, y);
                            script->codeModifiedPre(ScriptEdit::SCRIPT_MODIFY_DONE, 0, 0);
                            list<string>::iterator mPos = ((list<string>*)header->item1)->begin();
                            for (int pos = 0; pos < y; ++pos) {
                                swap(*cPos, *mPos);
                                ++cPos;
                                ++mPos;
                            }
                            script->codeModifiedPost(ScriptEdit::SCRIPT_MODIFY_LINE, x, y);
                        }
                        else if (type == UNDO_SCRIPTINSERT) {
                            debugWrite(DEBUG_UNDO, "undo: modify script insert");
                            script->codeModifiedPre(ScriptEdit::SCRIPT_REMOVE_LINES, x, y);
                            script->codeModifiedPre(ScriptEdit::SCRIPT_MODIFY_DONE, 0, 0);
                            list<string>::iterator cEnd = cPos;
                            for (int pos = 0; pos < y; ++pos) {
                                ++cEnd;
                            }
                            list<string>* inserted = new list<string>(cPos, cEnd);
                            script->getSource().erase(cPos, cEnd);
                            header->type = (UndoType)(UNDO_SCRIPTREMOVE | (header->type & UNDO_MERGE));
                            header->item1 = inserted;
                            script->codeModifiedPost(ScriptEdit::SCRIPT_REMOVE_LINES, x, y);
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: modify script remove");
                            script->codeModifiedPre(ScriptEdit::SCRIPT_INSERT_LINES, x, y);
                            script->codeModifiedPre(ScriptEdit::SCRIPT_MODIFY_DONE, 0, 0);
                            list<string>* deleted = (list<string>*)header->item1;
                            assert((int)deleted->size() == y);
                            script->getSource().splice(cPos, *deleted);
                            delete deleted;
                            header->type = (UndoType)(UNDO_SCRIPTINSERT | (header->type & UNDO_MERGE));
                            header->item1 = NULL;
                            script->codeModifiedPost(ScriptEdit::SCRIPT_INSERT_LINES, x, y);
                        }
                        
                        script->codeModifiedPost(ScriptEdit::SCRIPT_MODIFY_DONE, 0, 0);
                        if ((edit) && (!windowDidntExist)) edit->setCursorData(ins, selBegin, selEnd);  
                        script->markUnlock();
                        break;
                    }

                    case UNDO_LAYERTILESET: {
                        debugWrite(DEBUG_UNDO, "undo: modify layer tileset");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
                        // (have to account for 0 tileset)
                        TileSet* tileset = layer->getTileSet();
                        if (tileset) header->xPos = tileset->getId();
                        else header->xPos = 0;
                        try {
                            layer->setTileset(x); // Another exception point
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        layer->markUnlock();
                        break;
                    }
                    
                    case UNDO_LAYERSIZE: {
                        debugWrite(DEBUG_UNDO, "undo: modify layer size");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
    
                        header->xPos = layer->getXSize();
                        header->yPos = layer->getYSize();
                        layer->setSize(x, y, NULL, NULL, (Uint32**)(&header->item1), (Uint32**)(&header->item2), (Uint32**)(&header->item3));
    
                        layer->markUnlock();
                        break;
                    }

                    case UNDO_LAYERFX: {
                        debugWrite(DEBUG_UNDO, "undo: modify layer effects flag");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
                        header->xPos = layer->getUsesFx();
                        layer->setEffects(x);
                        layer->markUnlock();
                        break;
                    }

                    case UNDO_LAYEREXT: {
                        debugWrite(DEBUG_UNDO, "undo: modify layer extended data flag");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
                        header->xPos = layer->getUsesExt();
                        layer->setExtended(x);
                        layer->markUnlock();
                        break;
                    }
                    
                    case UNDO_LAYERTYPE: {
                        debugWrite(DEBUG_UNDO, "undo: modify layer type");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
                        header->xPos = layer->getType();
                        layer->setType((Layer::LayerType)x);
                        layer->markUnlock();
                        break;
                    }
                
                    case UNDO_LAYERTILECUR: {
                        // Verify window still exists (if not, this is a "no op"
                        if (desktop->verifyWindow(header->sourceWindow)) {
                            debugWrite(DEBUG_UNDO, "undo: modify layer affected/view");
                            // Ensure proper layers are selected
                            SceneEditLayer* win = dynamic_cast<SceneEditLayer*>(dynamic_cast<FrameWindow*>(desktop->verifyWindow(header->sourceWindow))->getClient());
                            win->refreshLayers(header->data1, header->data2, header->data3);
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: skip layer affected/view (window closed)");
                        }
                        break;
                    }
                    
                    case UNDO_LAYERTILETMP: {
                        // Verify window still exists (if not, this is a "no op"
                        if (desktop->verifyWindow(header->sourceWindow)) {
                            debugWrite(DEBUG_UNDO, "undo: modify layer floating selection");
                            // Swap data here with data in layer storage
                            Uint32* data = *((Uint32**)(header->item1));
                            data += y * subid + x;

                            // Temp storage for swapping
                            temp = new Uint32[w];
                            int lineSize = w * 4;
                
                            for (int pos = h; pos > 0; --pos) {
                                memcpy(temp, data, lineSize);
                                memcpy(data, buffer, lineSize);
                                memcpy(buffer, temp, lineSize);
                                data += subid;
                                buffer += w;
                            }

                            if (header->item3) {
                                Uint32* dataExt = *((Uint32**)(header->item3));
                                dataExt += y * subid + x;

                                for (int pos = h; pos > 0; --pos) {
                                    memcpy(temp, dataExt, lineSize);
                                    memcpy(dataExt, buffer, lineSize);
                                    memcpy(buffer, temp, lineSize);
                                    dataExt += subid;
                                    buffer += w;
                                }
                            }
                            
                            if (header->item2) {
                                Uint32* dataFx = *((Uint32**)(header->item2));
                                dataFx += y * subid + x;

                                for (int pos = h; pos > 0; --pos) {
                                    memcpy(temp, dataFx, lineSize);
                                    memcpy(dataFx, buffer, lineSize);
                                    memcpy(buffer, temp, lineSize);
                                    dataFx += subid;
                                    buffer += w;
                                }
                            }

                            // UNDO_LAYERTILESEL will alert window
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: skip layer floating selection (window closed)");
                        }
                        break;
                    }
                    
                    case UNDO_LAYERTILESEL: {
                        // Verify window still exists (if not, this is a "no op"
                        if (desktop->verifyWindow(header->sourceWindow)) {
                            debugWrite(DEBUG_UNDO, "undo: modify layer selection");
                            // Swap data here with data in selection surface
                            Uint8* selData = *((Uint8**)(header->item1));
                        
                            // Temp storage for swapping
                            temp = new Uint32[w];
                            Uint8* dest = selData + y * subid + x;
                
                            for (int pos = h; pos > 0; --pos) {
                                memcpy(temp, dest, id);
                                memcpy(dest, buffer, id);
                                memcpy(buffer, temp, id);
                                buffer += w;
                                dest += subid;
                            }
                            
                            // alert window and swap selection parameters
                            SceneEditLayer* win = dynamic_cast<SceneEditLayer*>(dynamic_cast<FrameWindow*>(desktop->verifyWindow(header->sourceWindow))->getClient());
                            win->swapSelectionParameters(header->data1, header->data2, header->data3);
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: skip layer selection (window closed)");
                        }
                        break;
                    }
                    
                    case UNDO_LAYERSPAWNSEL: {
                        // Verify window still exists (if not, this is a "no op"
                        if (desktop->verifyWindow(header->sourceWindow)) {
                            debugWrite(DEBUG_UNDO, "undo: modify spawn selection");
                            // Swap data here with data in selection surface
                            set<int>* liveData = (set<int>*)(header->item1);
                            set<int>* undoData = (set<int>*)(header->item2);
                            liveData->swap(*undoData);
                            
                            // alert window
                            SceneEditLayer* win = dynamic_cast<SceneEditLayer*>(dynamic_cast<FrameWindow*>(desktop->verifyWindow(header->sourceWindow))->getClient());
                            win->updatedSpawnSelection();
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: skip spawn selection (window closed)");
                        }
                        break;
                    }
                
                    case UNDO_PAINTSEL: {
                        // Verify window still exists (if not, this is a "no op"
                        if (desktop->verifyWindow(header->sourceWindow)) {
                            debugWrite(DEBUG_UNDO, "undo: modify tile selection");
                            // Swap data here with data in selection surface
                            SDL_Surface* surface = *((SDL_Surface**)(header->item1));
                        
                            // Temp storage for swapping
                            temp = new Uint32[w];
    
                            // @TODO: reallocate surface if not large enough
                            int pitch = surface->pitch;
                            int lineSize = w * 4;
                            Uint8* dest = ((Uint8*)surface->pixels) + y * pitch + x * 4;
                
                            for (int pos = h; pos > 0; --pos) {
                                memcpy(temp, dest, lineSize);
                                memcpy(dest, buffer, lineSize);
                                memcpy(buffer, temp, lineSize);
                                buffer += w;
                                dest += pitch;
                            }
                            
                            // alert window and swap selection parameters
                            TilePaint* win = dynamic_cast<TilePaint*>(dynamic_cast<FrameWindow*>(desktop->verifyWindow(header->sourceWindow))->getClient());
                            win->swapSelectionParameters(header->data1, header->data2, header->data3);
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: skip tile selection (window closed)");
                        }
                        break;
                    }
                    
                    case UNDO_LAYERTILE:
                    case UNDO_LAYERTILEEXT:
                    case UNDO_LAYERTILEFX: {
                        // Swap data here with data in layer
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        layer->markLock(); // Exception point
                        
                        Rect rect = { x, y, w, h };
                        if (type == UNDO_LAYERTILE) {
                            debugWrite(DEBUG_UNDO, "undo: modify layer tiles");
                            layer->swapLayer(buffer, NULL, NULL, w, rect);
                        }
                        else if (type == UNDO_LAYERTILEFX) {
                            debugWrite(DEBUG_UNDO, "undo: modify layer effects");
                            layer->swapLayer(NULL, NULL, buffer, w, rect);
                        }
                        else {
                            debugWrite(DEBUG_UNDO, "undo: modify layer extended data");
                            layer->swapLayer(NULL, buffer, NULL, w, rect);
                        }
                        
                        layer->markUnlock();
                        break;
                    }

                    case UNDO_TILE: {
                        debugWrite(DEBUG_UNDO, "undo: modify tile");
                        // Swap data here with data on tile
                        TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tiles);
                        tiles->markLock(); // Exception point

                        int pitch = tiles->viewTilePitch();
                        int lineSize = w * 4;
                        Uint8* dest = tiles->editTileData(subid) + y * pitch + x * 4;

                        // Temp storage for swapping
                        temp = new Uint32[w];

                        for (int pos = h; pos > 0; --pos) {
                            memcpy(temp, dest, lineSize);
                            memcpy(dest, buffer, lineSize);
                            memcpy(buffer, temp, lineSize);
                            buffer += w;
                            dest += pitch;
                        }

                        // Store change and notify
                        tiles->editTileDone(subid);
                        tiles->markUnlock();
                        break;
                    }

                    case UNDO_TILECOLL: {
                        debugWrite(DEBUG_UNDO, "undo: modify collision map");
                        // Swap data here with data on map
                        TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tiles);
                        tiles->markLock(); // Exception point

                        int pitch = tiles->getHeight();
                        Uint32* dest = tiles->editCollData(subid) + y * pitch + x * 4;

                        // Temp storage for swapping
                        temp = new Uint32[w];
                        
                        for (int pos = w; pos > 0; --pos) {
                            memcpy(temp, dest, h);
                            memcpy(dest, buffer, h);
                            memcpy(buffer, temp, h);
                            buffer += h;
                            dest += pitch;
                        }

                        // Store change and notify
                        tiles->editCollDone(subid);
                        tiles->markUnlock();
                        break;
                    }
                    
                    case UNDO_TILEGLYPH: {
                        debugWrite(DEBUG_UNDO, "undo: modify tile glyph");
                        TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tiles);
                        tiles->markLock(); // Exception point
    
                        header->xPos = tiles->getGlyphWidth(subid);
                        tiles->setGlyphWidth(subid, x, NULL);
    
                        tiles->markUnlock();
                        break;
                    }
                    
                    case UNDO_WORLDNAME:
                    case UNDO_LAYERNAME:
                    case UNDO_TILENAME:
                    case UNDO_SCENENAME:
                    case UNDO_ANIMGROUPNAME:
                    case UNDO_SCRIPTNAME:
                    case UNDO_SPAWNNAME:
                    case UNDO_FOLDERNAME: {
                        debugWrite(DEBUG_UNDO, "undo: modify name");
                        string tempName;
                        string newName((char*)buffer, x);
                        
                        // Grab current and new names
                        // Store change and notify
                        if (type == UNDO_LAYERNAME) {
                            LayerEdit* layer = world->findLayer(id);
                            assert(layer);
                            tempName = layer->getName();
                            layer->setName(newName);
                        }
                        else if (type == UNDO_SPAWNNAME) {
                            SpawnEdit* spawn = world->findSpawn(id);
                            assert(spawn);
                            tempName = spawn->getName();
                            spawn->setName(newName);
                        }
                        else if (type == UNDO_TILENAME) {
                            TileSetEdit* tiles = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                            assert(tiles);
                            tempName = tiles->getName();
                            tiles->setName(newName);
                        }
                        else if (type == UNDO_SCENENAME) {
                            SceneEdit* scene = dynamic_cast<SceneEdit*>(world->findScene(id));
                            assert(scene);
                            tempName = scene->getName();
                            scene->setName(newName);
                        }
                        else if (type == UNDO_SCRIPTNAME) {
                            ScriptEdit* script = dynamic_cast<ScriptEdit*>(world->findScript(id));
                            assert(script);
                            tempName = script->getName();
                            script->setName(newName);
                        }
                        else if (type == UNDO_WORLDNAME) {
                            tempName = world->getTitle();
                            world->setTitle(newName);
                        }
                        else if (type == UNDO_FOLDERNAME) {
                            FolderEdit* folder = dynamic_cast<FolderEdit*>(world->findFolder(id));
                            assert(folder);
                            tempName = folder->getName();
                            folder->setName(newName);
                        }
                        else {
                            assert(type == UNDO_ANIMGROUPNAME);
                            AnimGroupEdit* animgroup = dynamic_cast<AnimGroupEdit*>(world->findAnimGroup(id));
                            assert(animgroup);
                            tempName = animgroup->getName();
                            animgroup->setName(newName);
                        }
                        
                        // Place old name back into buffer
                        memcpy(buffer, tempName.c_str(), tempName.size());
                        header->xPos = tempName.size();
                        break;
                    }
                    
                    case UNDO_LAYERMOVE: {
                        debugWrite(DEBUG_UNDO, "undo: undo layer swap");
                        SceneEdit* scene = dynamic_cast<SceneEdit*>(world->findScene(id));
                        assert(scene);
                        scene->swapLayer(x, y);
                        break;
                    }
                    
                    case UNDO_SPAWNCREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo spawn creation");
                        LayerEdit* layer = world->findLayer(subid);
                        assert(layer); // @TODO: non-layer spawns (subid == 0)
                        layer->markLock(); // Exception point
                        try {
                            SpawnEdit* spawn = world->findSpawn(id);
                            assert(spawn);
                            layer->deleteSpawn(spawn);
                            header->type = (UndoType)(UNDO_SPAWNDELETE | (header->type & UNDO_MERGE));
                            header->item1 = spawn;
                            layer->markUnlock();
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        break;
                    }

                    case UNDO_LAYERCREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo layer creation");
                        LayerEdit* layer = world->findLayer(id);
                        assert(layer);
                        SceneEdit* scene = dynamic_cast<SceneEdit*>(world->findScene(subid));
                        assert(scene);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!scene->deleteLayer(x)) { // Exception point
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_LAYERDELETE | (header->type & UNDO_MERGE));
                        header->item1 = layer;
                        break;
                    }

                    case UNDO_SCENECREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo scene creation");
                        SceneEdit* scene = dynamic_cast<SceneEdit*>(world->findScene(id));
                        assert(scene);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!world->deleteScene(scene)) {
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_SCENEDELETE | (header->type & UNDO_MERGE));
                        header->item1 = scene;
                        break;
                    }

                    case UNDO_ANIMGROUPCREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo anim group creation");
                        AnimGroupEdit* animgroup = dynamic_cast<AnimGroupEdit*>(world->findAnimGroup(id));
                        assert(animgroup);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!world->deleteAnimGroup(animgroup)) {
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_ANIMGROUPDELETE | (header->type & UNDO_MERGE));
                        header->item1 = animgroup;
                        break;
                    }

                    case UNDO_FOLDERCREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo folder creation");
                        FolderEdit* folder = dynamic_cast<FolderEdit*>(world->findFolder(id));
                        assert(folder);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!world->deleteFolder(folder)) {
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_FOLDERDELETE | (header->type & UNDO_MERGE));
                        header->item1 = folder;
                        break;
                    }
                        
                    case UNDO_SCRIPTCREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo script creation");
                        ScriptEdit* script = dynamic_cast<ScriptEdit*>(world->findScript(id));
                        assert(script);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!world->deleteScript(script)) {
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_SCRIPTDELETE | (header->type & UNDO_MERGE));
                        header->item1 = script;
                        break;
                    }
                        
                    case UNDO_TILECREATE: {
                        debugWrite(DEBUG_UNDO, "undo: undo tileset creation");
                        TileSetEdit* tileset = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tileset);
                        // (delete shouldn't ever "fail", but if it does, we just cancel)
                        if (!world->deleteTileset(tileset)) {
                            cancelUndo = 1;
                            break;
                        }
                        header->type = (UndoType)(UNDO_TILEDELETE | (header->type & UNDO_MERGE));
                        header->item1 = tileset;
                        break;
                    }

                    case UNDO_SPAWNDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo spawn deletion");
                        // Undelete
                        LayerEdit* layer = world->findLayer(subid);
                        assert(layer); // @TODO: non-layer spawns (subid == 0)
                        layer->markLock(); // Exception point
                        try {
                            SpawnEdit* spawn = (SpawnEdit*)header->item1;
                            assert(spawn);
                            layer->addSpawn(spawn);
                            header->type = (UndoType)(UNDO_SPAWNCREATE | (header->type & UNDO_MERGE));
                            header->item1 = NULL;
                            layer->markUnlock();
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        break;
                    }

                    case UNDO_LAYERDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo layer deletion");
                        // Undelete
                        LayerEdit* layer = (LayerEdit*)header->item1;
                        assert(layer);
                        SceneEdit* scene = dynamic_cast<SceneEdit*>(world->findScene(subid));
                        assert(scene);
                        scene->insertLayer(layer, x, NULL, NULL, 1);
                        header->type = (UndoType)(UNDO_LAYERCREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }

                    case UNDO_SCENEDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo scene deletion");
                        // Undelete
                        SceneEdit* scene = (SceneEdit*)header->item1;
                        assert(scene);
                        world->readdScene(scene);
                        header->type = (UndoType)(UNDO_SCENECREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }

                    case UNDO_SCRIPTDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo script deletion");
                        // Undelete
                        ScriptEdit* script = (ScriptEdit*)header->item1;
                        assert(script);
                        world->readdScript(script);
                        header->type = (UndoType)(UNDO_SCRIPTCREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }

                    case UNDO_ANIMGROUPDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo anim group deletion");
                        // Undelete
                        AnimGroupEdit* animgroup = (AnimGroupEdit*)header->item1;
                        assert(animgroup);
                        world->readdAnimGroup(animgroup);
                        header->type = (UndoType)(UNDO_ANIMGROUPCREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }

                    case UNDO_FOLDERDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo folder deletion");
                        // Undelete
                        FolderEdit* folder = (FolderEdit*)header->item1;
                        assert(folder);
                        world->readdFolder(folder);
                        header->type = (UndoType)(UNDO_FOLDERCREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }

                    case UNDO_TILEDELETE: {
                        debugWrite(DEBUG_UNDO, "undo: undo tileset deletion");
                        // Undelete
                        TileSetEdit* tileset = (TileSetEdit*)header->item1;
                        assert(tileset);
                        world->readdTileset(tileset);
                        header->type = (UndoType)(UNDO_TILECREATE | (header->type & UNDO_MERGE));
                        header->item1 = NULL;
                        break;
                    }
                    
                    case UNDO_TILESIZE: {
                        debugWrite(DEBUG_UNDO, "undo: modify tileset size");
                        TileSetEdit* tileset = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tileset);
                        tileset->markLock(); // Exception point
    
                        header->xPos = tileset->getWidth();
                        header->yPos = tileset->getHeight();
                        header->data1 = tileset->getCount();
                        header->data2 = tileset->getCollisionCount();
                        tileset->setSize(x, y, d1, d2, NULL, NULL, (SDL_Surface**)(&header->item1), (Uint32**)(&header->item2), (Uint32**)(&header->item3));
    
                        tileset->markUnlock();
                        break;
                    }
                    
                    case UNDO_TILEPERLINE: {
                        debugWrite(DEBUG_UNDO, "undo: modify tileset per line");
                        TileSetEdit* tileset = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tileset);
                        header->xPos = tileset->getTilesPerLine();
                        tileset->setTilesPerLine(x);
                        break;
                    }
                    
                    case UNDO_TILETRANS: {
                        debugWrite(DEBUG_UNDO, "undo: modify tileset transparency flag");
                        TileSetEdit* tileset = dynamic_cast<TileSetEdit*>(world->findTileSet(id));
                        assert(tileset);
                        header->xPos = tileset->getDefaultTransparent();
                        tileset->setDefaultTransparent(x);
                        break;
                    }

                    case UNDO_WORLDSTART: {
                        debugWrite(DEBUG_UNDO, "undo: modify world starting scene");
                        header->xPos = world->getStartScene();
                        world->setStartScene(x);
                        break;
                    }

                    case UNDO_SCRIPTDEFAULT: {
                        debugWrite(DEBUG_UNDO, "undo: modify script default image");
                        ScriptEdit* script = dynamic_cast<ScriptEdit*>(world->findScript(id));
                        assert(script);
                        if (script->getDefaultAnimgroup()) header->data1 = script->getDefaultAnimgroup()->getId();
                        else header->data1 = 0;
                        if (script->getDefaultTileset()) header->data2 = script->getDefaultTileset()->getId();
                        else header->data2 = 0;
                        header->data3 = script->getDefaultId();
                        script->setDefault(world->findAnimGroup(d1),
                                           world->findTileSet(d2),
                                           d3);
                        break;
                    }
                    
                    case UNDO_SPAWNMOVE: {
                        debugWrite(DEBUG_UNDO, "undo: modify spawn move");
                        LayerEdit* layer = world->findLayer(subid);
                        assert(layer); // @TODO: non-layer spawns (subid == 0)
                        layer->markLock(); // Exception point
                        try {
                            SpawnEdit* spawn = world->findSpawn(id);
                            assert(spawn);
                            header->xPos = spawn->getX();
                            header->yPos = spawn->getY();
                            spawn->setPos(x, y);
                            layer->markUnlock();
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        break;
                    }
                    
                    case UNDO_SPAWNSPRITE: {
                        debugWrite(DEBUG_UNDO, "undo: modify spawn image");
                        LayerEdit* layer = world->findLayer(subid);
                        assert(layer); // @TODO: non-layer spawns (subid == 0)
                        layer->markLock(); // Exception point
                        try {
                            SpawnEdit* spawn = world->findSpawn(id);
                            assert(spawn);
                            if (spawn->getAnimgroup()) header->data1 = spawn->getAnimgroup()->getId();
                            else header->data1 = 0;
                            if (spawn->getTileset()) header->data2 = spawn->getTileset()->getId();
                            else header->data2 = 0;
                            header->data3 = spawn->getSubid();
                            spawn->setSprite(world->findAnimGroup(d1),
                                             world->findTileSet(d2),
                                             d3);
                            layer->markUnlock();
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        break;
                    }
                    
                    case UNDO_SPAWNSCRIPT: {
                        debugWrite(DEBUG_UNDO, "undo: modify spawn script");
                        LayerEdit* layer = world->findLayer(subid);
                        assert(layer); // @TODO: non-layer spawns (subid == 0)
                        layer->markLock(); // Exception point
                        try {
                            SpawnEdit* spawn = world->findSpawn(id);
                            assert(spawn);
                            if (spawn->getScript()) header->data1 = spawn->getScript()->getId();
                            else header->data1 = 0;
                            spawn->setScript(world->findScript(d1));
                            layer->markUnlock();
                        }
                        catch (...) {
                            layer->markUnlock();
                            throw;
                        }
                        break;
                    }
                    
                    case UNDO_FOLDERADD: {
                        debugWrite(DEBUG_UNDO, "undo: undo folder add");
                        FolderEdit* folder = world->findFolder(id);
                        assert(folder);
                        SaveLoad* item = world->findItem(d1, subid);
                        assert(item);
                        folder->removeItem(item);
                        header->type = (UndoType)(UNDO_FOLDERREMOVE | (header->type & UNDO_MERGE));
                        break;
                    }
                    
                    case UNDO_FOLDERREMOVE: {
                        debugWrite(DEBUG_UNDO, "undo: undo folder remove");
                        FolderEdit* folder = world->findFolder(id);
                        assert(folder);
                        SaveLoad* item = world->findItem(d1, subid);
                        assert(item);
                        folder->addItem(item, d2);
                        header->type = (UndoType)(UNDO_FOLDERREMOVE | (header->type & UNDO_MERGE));
                        break;
                    }
                }
            }
            catch (UndoException& e) {
                // Since we were "inUndo", the above code should NEVER throw this
                assert(0);
            }
            catch (FileException& e) {
                // Likely from locking/uncaching
                cancelUndo = 1;
                guiErrorBox(string(e.details), errorTitleFile);
            }
        }
        
        if (cancelUndo) {
            inUndo = 0;
            delete[] temp;
            // Move undo pos back
            if (!doRedo) undoPos += w * h + undoHeaderSize;
            return;
        }
        
        // Notify window
        if ((header->sourceWindow) && (!didWindowFocus)) {
            // Verify existence
            Window* win = desktop->verifyWindow(header->sourceWindow);
            if (win) {
                desktop->bringToTop(win);
                win->undoNotify(type, id, subid, x, y, w, h);
                didWindowFocus = 1;
            }
        }
        
        delete[] temp;
    
        if (doRedo) {
            // Move undo pos forward an item
            undoPos += w * h + undoHeaderSize;
            header = (UndoHeader*)(undoBuffer + undoPos);
            isMerged = header->type & UNDO_MERGE;
        }
    }

    inUndo = 0;
}

